package com.apobates.jforum.grief.aurl.executor;

import com.apobates.jforum.grief.aurl.core.AUrlMeta;
import com.apobates.jforum.grief.aurl.core.AUrlMetaBuilder;
import com.apobates.jforum.grief.aurl.spring.StringUtils;
import java.security.MessageDigest;
import java.util.*;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import static java.util.stream.Collectors.toList;

/**
 * 短连接的生成执行器
 * blog.codinghorror.com/url-shortening-hashes-in-practice/
 */
public class ShortUrlExecutor extends AUrlMetaExecutor<AUrlMetaActionRecord> {
    private final Set<AUrlMetaActionRecord> actionRecords;
    private static String[] elements = {
            "a","b","c","d","e","f","g","h","i","j","k","l","m","n","o",
            "p","q","r","s","t","u","v","w","x","y","z","1","2","3","4",
            "5","6","7","8","9","0","A","B","C","D","E","F","G","H","I",
            "J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X",
            "Y","Z"
    };
    private final static String[] hexDigits = { "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F" };

    public ShortUrlExecutor(AUrlMeta rawMeta) {
        super(rawMeta);
        this.actionRecords = new HashSet<>();
    }

    @Override
    public Set<AUrlMetaActionRecord> getActionRecords() {
        return actionRecords;
    }

    @Override
    public AUrlMetaBuilder getBuilder() {
        Optional<String> shortURL = this.actionRecords
                .stream()
                .filter(mar -> mar.action().equals("short") && mar.param().equals("short"))
                .map(ele -> new ArrayList(ele.values()).get(0).toString())
                .findFirst();
        return shortURL.isPresent()? AUrlMetaBuilder.fromUriString(shortURL.get()): AUrlMetaBuilder.getInstance();
    }

    /**
     * 生成短连接地址
     * @return
     */
    public String generate(){
        final AUrlMeta rawMeta = getRawMeta();
        Objects.requireNonNull(rawMeta);
        if(rawMeta.getScheme() !=null && rawMeta.getHost() !=null) {
            return this.generate(Config.create(String.format("%s://%s", rawMeta.getScheme(), rawMeta.getHost())));
        }
        return this.generate(BlankConfig);
    }

    /**
     * 使用指定的配置生成短连接地址
     * @param config 短连接配置参数
     * @return
     */
    public String generate(Config config){
        final AUrlMeta rawMeta = getRawMeta();
        Objects.requireNonNull(rawMeta);
        final String original = rawMeta.toString();
        Config tc = Optional.ofNullable(config).orElse(BlankConfig);

        String tinyURL = tc.base.concat(wrapStr(tc.path, "/")).concat(this.convert(original, tc.algorithm, tc.length));
        this.reset("short", "original", original);
        this.reset("short", "short", tinyURL);
        return tinyURL;
    }

    private void reset(String action, String key, String value){
        this.actionRecords.add(new AUrlMetaActionRecord(action, key, List.of(value)));
    }

    private String convert(String longUrl, String algorithm, int length){
        String hexStr = encodeURL(longUrl, algorithm);
        List<String> tmp = segment(hexStr, length)
                .map(segmentStr->Long.parseLong(segmentStr,16))
                .map(decimalVal->mid62(decimalVal).intValue())
                .map(index->elements[index])
                .distinct()
                .collect(toList());

        if(tmp.size() != length) {
            // 删除
            if (tmp.size() > length) {
                // 不可能发生
            }
            // 增加(缺少的, 增加的不能重复)
            if (tmp.size() < length) {
                tmp.addAll(recover(tmp, Arrays.asList(elements), length - tmp.size()));
            }
        }
        return String.join("", tmp);
    }

    private static Long mid62(Long val){
        if(val.compareTo(0L) != -1 && val.compareTo(62L) == -1){
            return val;
        }
        return mid62(val / 62);
    }



    /** 对字符串进行MD5编码 */
    private static String encodeURL(String originString, String algorithm) {
        if (originString != null) {
            try {
                // 创建具有指定算法名称的信息摘要
                MessageDigest md5 = MessageDigest.getInstance(algorithm);
                // 使用指定的字节数组对摘要进行最后更新，然后完成摘要计算
                byte[] results = md5.digest(originString.getBytes());
                // 将得到的字节数组变成字符串返回
                String result = byteArrayToHexString(results);
                return result;
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return null;
    }

    /**
     * 轮换字节数组为十六进制字符串
     *
     * @param b 字节数组
     * @return 十六进制字符串
     */
    private static String byteArrayToHexString(byte[] b) {
        StringBuffer resultSb = new StringBuffer();
        for (int i = 0; i < b.length; i++) {
            resultSb.append(byteToHexString(b[i]));
        }
        return resultSb.toString();
    }

    // 将一个字节转化成十六进制形式的字符串
    private static String byteToHexString(byte b) {
        int n = b;
        if (n < 0) {
            n = 256 + n;
        }
        int d1 = n / 16;
        int d2 = n % 16;
        return hexDigits[d1] + hexDigits[d2];
    }

    /**
     * 默认的配置参数
     */
    private static Config BlankConfig = Config.create("http://localhost");

    /**
     * 将指定字符串按一段的块进行分隔
     * @param s 待分隔的字符串
     * @param chunkSize 每块的长度
     * @return
     */
    public static Stream<String> split(final String s, final int chunkSize) {
        return IntStream.iterate(0, i -> i < s.length(), i -> i + chunkSize).mapToObj(i -> s.substring(i, Math.min(s.length(), i + chunkSize)));
    }

    /**
     * 分隔指定字符串到指定的段落中
     * @param s 待分隔的字符串
     * @param segmentSize 段落数量
     * @return
     */
    public static Stream<String> segment(final String s, final int segmentSize) {
        List<String> data=new ArrayList<>(segmentSize);
        int chunkSize = s.length()/segmentSize;
        for (int i=0;i<segmentSize;i++){
            try {
                data.add(s.substring(i * chunkSize, chunkSize+(i * chunkSize)));
            }catch (IndexOutOfBoundsException e){
                data.add(s.substring(i * chunkSize));
            }
        }
        // 已有长度
        int curSize = chunkSize*segmentSize;
        if(s.length()>curSize){
            String temp = data.remove(segmentSize-1).concat(s.substring(curSize));
            data.add(temp);
        }
        return data.stream();
    }

    /**
     * 从hyperSet挑选target中不存在的size元素
     * @param target 待填补的集合
     * @param hyperSet 超集
     * @param size 待填补的数量
     * @return
     */
    public static List<String> recover(List<String> target, List<String> hyperSet, int size){
        List<String> reduce2 = hyperSet.stream().filter(item -> !target.contains(item)).collect(toList());
        Collections.shuffle(reduce2);
        return reduce2.subList(0, size);
    }

    /**
     * 使用指定字符包裹目标字符串
     * @param target 目标字符串
     * @param wrapChar 包裹的字符
     * @return
     */
    public static String wrapStr(String target, String wrapChar){
        if(!StringUtils.hasLength(target) || target.equals(wrapChar)){
            return wrapChar;
        }
        // 头
        String header = target.startsWith(wrapChar)?"":wrapChar;
        // 尾
        String tailer = target.endsWith(wrapChar)?"":wrapChar;
        return header + target + tailer;
    }

    /**
     * 配置参数
     */
    public static class Config{
        // 前缀:http://domain
        private final String base;
        // 路径
        private String path="/";
        // 散列算法
        private String algorithm="MD5";
        // 长度(5-8)
        private int length=6;
        // 分布度检查
        private Config(String urlBase) {
            this.base = urlBase;
        }

        /**
         * 短连接的前缀信息。
         * @param urlBase 示例: http://pan.baidu.com
         * @return
         */
        public static Config create(String urlBase){
            return new Config(urlBase);
        }

        /**
         * 短连接的前缀信息。create的别名方法
         * 出于具名的考虑
         * @param urlBase 示例: http://pan.baidu.com
         * @return
         */
        public static Config base(String urlBase){
            return create(urlBase);
        }
        /**
         * 短连接的路径.包含：/
         * @param pathStr 示例：/s. 默认为:/
         * @return
         */
        public Config path(String pathStr){
            this.path = pathStr;
            return this;
        }

        /**
         * 目标连接的散列码长度
         * @param hashValLen 散列的长度. 默认为:6. 允许的范围在5-8之间
         * @return
         */
        public Config length(int hashValLen){
            if(hashValLen>=5 && hashValLen<=8) {
                this.length = hashValLen;
            }
            return this;
        }

        /**
         * 目标连接的散列码生成时使用的算法
         * @param algorithm 算法名称. 默认为:MD5
         * @return
         */
        public Config algorithm(Algorithm algorithm){
            this.algorithm = algorithm.name();
            return this;
        }

    }

    /**
     * 配置参数支持的算法枚举
     */
    public static enum Algorithm {
        MD5(32, 8), SHA1(40, 8), SHA256(64, 8);
        // 散列后的字符长度(有多少个16进制字符)
        private int hashLength;
        // 最大的基础单位/最大的长度
        private int maxLength;

        Algorithm(int hashLength, int maxLength) {
            this.hashLength = hashLength;
            this.maxLength = maxLength;
        }

        public int getHashLength() {
            return hashLength;
        }

        public int getMaxLength() {
            return maxLength;
        }
    }
}
